iT邦幫忙

2021 iThome 鐵人賽

DAY 26
0

bloc

有經驗的前端工程師或多或少應該都有聽過 MVCMVPMVVM 架構的開發方式,這些開發方式可以讓我們達到觀注點分離(Separation of concerns,SoC)的設計原則讓開發團隊可以遵詢同一種模式進行開發工作。

今天我們就來看看目前在 Flutter 設計上經常聽到 Bloc 是什麼吧。

Bloc Design Pattern

Bloc (Business Logic Component) 的設計理念會希望透過該設計原則將 View 的代碼與業務邏輯拆開, 易於程式碼的維護與開發、測試。

a predictable state management library for Dart.

Simple & Lightweight
Highly Testable
For Dart, Flutter, and AngularDart

架構

從官網的架構圖上來看,從角色可以區分為三種類別

bloc_arch

該圖片引用來自官網架構文件說明

Data Layer

負責處理資料來源的管理,通常會從 DB 或是 API 取得資料。

Business Logic Layer

接收 UI 傳遞過來的事件(events)觸發業務邏輯的處理,可能會需要從 Data 取得相關資料,視邏輯有機會觸發狀態(states)的轉換。

Presentation Layer

負責處理畫面的呈現,畫面照業務邏輯的 states 而有不同狀態的顯示方式。

在開發前需要定義應用上可能的狀態以及會需要處理的事件為何!!!

以開發聊天室作為範例

先前的聊天室範例我們是使用 StatefulWidget 搭配 ViewModel 的寫法,接下來我們試著用 bloc 改寫看看。

安裝 bloc 設定

與 bloc 相關的套件如下

dependencies:
  bloc: ^7.2.0
  flutter_bloc: ^7.3.0
  equatable: ^2.0.3

先列舉聊天室功能的業務邏輯

  1. 在進入聊天室畫面時,需要與聊天室伺服器建立連線
  2. 連線完成後待接收WebSocket推送的訊息
  3. 聊天室需顯示連線的狀態
  4. 斷線時可以重新連線
  5. 可以發送聊天訊息
  6. 保存接收過的訊息資料

bloc pattern 任務拆分

  • UI:輸入文字欄位、聊天訊息列表
  • bloc:定義聊天室業務邏輯
  • 資料源:WebSocket相關
  • events:連線建立、連線中斷、接到訊息、DB初始化
  • status:記錄聊天室狀態相關資料

chat_bloc.dart

負責處理聊天室WebSocket建立工作,新增一個類別繼承Bloc並定義對應的EventState

class ChatBloc extends Bloc<ChatEvent, ChatState> {
  final Connection _connection;
  ChatBloc(this._connection) : super(const ChatState()) {}
}

Connection 是先前範例中我們包裝用來建立 WebSocket 的類別,在 bloc 初始化時從外部注入。

chat_state.dart

ChatState類別中我們定義兩個屬性

  1. status - 記錄目前連線狀態
  2. data - 記錄聊天室訊息記錄
enum SocketStatus { initial, open, closed }

class ChatState extends Equatable {
  final SocketStatus status;
  final List<Message> data;

  const ChatState({
    this.status = SocketStatus.initial,
    this.data = const <Message>[],
  });

  ChatState copyWith({
    SocketStatus? status,
    List<Message>? data,
  }) {
    return ChatState(
      status: status ?? this.status,
      data: data ?? this.data,
    );
  }

  @override
  String toString() {
    return '''ChatState { status: $status, data_length: ${data.length} }''';
  }

  @override
  List<Object> get props => [data, status];
}

chat_event.dart

根據需求整理出聊天室會需要處理的事件內容

  • ChatDBInit - 從 DB 初始化的聊天訊息資料
  • ChatReceiveMessage - 接收到聊天室訊息
  • ChatSocketStatusChange - WebSocket連線狀態改變
abstract class ChatEvent extends Equatable {
  const ChatEvent();

  @override
  List<Object> get props => [];
}

class ChatReceiveMessage extends ChatEvent {
  final Message msg;
  const ChatReceiveMessage(this.msg);
}

class ChatSocketStatusChange extends ChatEvent {
  final bool status;
  const ChatSocketStatusChange(this.status);
}

class ChatDBInit extends ChatEvent {}

完成業務邏輯的基本定義後,接下來試著跟畫面結合在一下

聊天室頁面

BlocProvider

使用 flutter_bloc 提供的 BlocProvider 提供聊天室 Bloc 的實例

class ChatPage extends StatelessWidget {
  ChatPage({Key? key}) : super(key: key);

  final Uri uri = Uri.parse('ws://test.dev.rde:8000/?token=sm2');

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => ChatBloc(Connection(uri: uri)),
      child: const ChatView(),
    );
  }
}

事件與狀態

ChatBloc 建構式需定義事件設定,這邊我們可以透過 blocadd 方法觸發事件 ChatDBInit

早期 bloc 的寫法是在這邊定義mapEventToState,不過語法比較難理解,後續已建議改成下列的寫法。bloc_issues

我們在接收到 ChatDBInit 事件後透過 on 綁定 _initDB 處理,我們先從 db 取回訊息資料,並透過 ChatState copyWith 產生一個新的 state,並以 blocemit 觸發狀態異動。

接著在綁定Connection相關的事件:ChatSocketStatusChangeChatReceiveMessage

  ChatBloc(this._connection) : super(const ChatState()) {
    on<ChatReceiveMessage>(_onMessage);
    on<ChatSocketStatusChange>(_onStateChange);
    on<ChatDBInit>(_initDB);

    add(ChatDBInit());
  }

  void _initDB(event, emit) async {
    emit(state.copyWith(data: await db.query()));

    _connectionSubscription = _connection.connected.listen((bool status) {
      add(ChatSocketStatusChange(status));
    });
    _connectionSubscription = _connection.stream.listen((data) {
      if (data["eventName"] == "chat:msg") {
        add(ChatReceiveMessage(Message.fromJson(data)));
      }
    });
  }

簡單來說:
bloc 其實是有限狀態機的一種設計方式,根據業務邏輯的需要歸納出states,透過events觸發業務邏輯的處理,引發 state 的轉換。

BlocBuilder

View 的處理上,可使用 flutter_bloc 提供的 BlocBuilder 監控狀態的變換而重新渲染畫面,並使用 state 裡與畫面有關的資料。

例如:我們在 ChatState 中定義 status 屬性處理 WebSocket 的連線狀態

 Widget build(BuildContext context) {
    return Expanded(
      flex: 1,
      child: BlocBuilder<ChatBloc, ChatState>(
        builder: (context, state) {
          final btnTitle = state.status == SocketStatus.open ? "已連線" : "請重連";
          var controller = context.read<ChatBloc>().controller;

          return Column(
            children: [
              SizedBox(
                width: double.infinity,
                child: TextButton(
                  child: Text(btnTitle),
                  onPressed: () {
                    print(state.status);
                    if (state.status == SocketStatus.closed) {
                      context.read<ChatBloc>().reconnect();
                    }
                  },
                ),
              ),

使用bloc改寫後程式碼語意更易懂,也不用一直呼叫 setState,完整程式碼在這

chat_bloc

其他

我自己在研究bloc時初期遇到的狀況是不太曉得要怎麼將業務邏輯定義清楚以及statesevents 的內容要怎麼寫。後來查看官網上的一些範例後才慢慢掌握。

心得如下:

  1. 業務邏輯的單位大小由你自己決定:在聊天室的範例中,初期我一直在糾結連線狀態與聊天室訊息記錄是要放在一起還是拆分成兩個bloc,其實沒有對與錯,就看自己怎麼寫符合當下狀況,有需要在拆分也行。

  2. bloccubit 兩種寫法哪一種適合我:如果你只要處理狀態資料的轉換而不用事件的狀態那簡單應用 cubit 就好。例如:其實我可以用 cubit 處理接收到聊天室的訊息事件即可。

  3. states 的寫法:定義狀態抽象類別然後在依需求實作不同狀態的子類別還是 使用單一狀態類別透過 copyWith 產生新的狀態類別實例。

  4. 我要使用什麼方式觸發states的轉換:在聊天室的範例我從 WebSocket 收到訊息時,我可以發出"接到訊息事件"對應後續的業務邏輯,也可以直接發出新的狀態,那我到底需不需要額外使用事件

    follow 原則: 事件 > 業務邏輯 > 狀態

小結

使用bloc將觀察點分離,在組件中我們只要處理與 UI 有關的邏輯,將複雜多變的業務邏輯放到 bloc,在開發與維護或是測試工作上都是很不錯的,建議花點時間研究。


上一篇
Flutter體驗 Day 25-SharedPreferences
下一篇
Flutter體驗 Day 27-flame SpriteComponent
系列文
Flutter / Dart 跨平台App開發體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言